﻿namespace Mueller.CoffeeMonitor

open System
open System.Configuration
open System.IO
open System.Diagnostics
open System.Text
open Mueller.ScriptTools
open V8Bridge.FSharp

type CompileMonitor(settings: SettingsV2) =

    let compile = (if settings.Iced then IcedCompiler().Compile else CoffeeScriptCompiler().Compile) settings.CreateWrapper
    let minifyAgent = lazy( UglifyCompiler() )

    let watchers = 
        let createWatcher path =
            try
                Some(new FileSystemWatcher(path, "*.coffee", IncludeSubdirectories = true))
            with :? UnauthorizedAccessException as uae ->
                Notification.warn "Error cannot monitor path: '%s'\n%s" path uae.Message
                None
        settings.Paths |> List.choose (fst >> createWatcher)

    let minify path code = async {
        if settings.Minify then
            let! result = minifyAgent.Value.Compile code
            match result with
            | Fail msg ->
                Notification.compileError (Path.GetFileName path) ("Minifcation error: \n" + msg)
                return None
            | Success minified ->
                return Some minified
        else
            return Some code
    }

    let getOutPath filePath =
        let dir = Path.GetDirectoryName filePath
        let name = Path.GetFileNameWithoutExtension filePath
        let outPath =
            let pathSetting =
                settings.Paths
                |> List.find (fun (p,_) -> dir.StartsWith(p, StringComparison.CurrentCultureIgnoreCase))
            match pathSetting with
            | source, Some(output) -> 
                if String.Equals(dir, source, StringComparison.CurrentCultureIgnoreCase) then
                    Path.Combine(output, name + ".js")
                else
                    let pathDiff = dir.[source.Length + 1 ..]
                    Path.Combine(output, pathDiff, name + ".js")
            | _, None -> 
                Path.Combine(dir, name + ".js")
        let outDir = Path.GetDirectoryName outPath
        if not (Directory.Exists outDir) then
            Directory.CreateDirectory outDir |> ignore
        outPath
    
    let compileScript path =
        if not (File.Exists path) then
            raise <| FileNotFoundException(path)
        let outPath = getOutPath path
        if Path.GetExtension path <> ".coffee" then
            failwithf "The given file does not have a '.coffee' extension: %s" path
        async {
            use instream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, FileOptions.Asynchronous)
            use reader = new AsyncStreamReader(instream)
            let! code = reader.ReadToEnd()
            reader.Close()
            let! result = compile code
            match result with
            | Fail msg ->
                Notification.compileError (Path.GetFileName path) msg
                return None
            | Success compiled ->
                let! minified = minify outPath compiled
                match minified with
                | Some mini ->
                    use outstream = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.None, 0x1000, FileOptions.Asynchronous)
                    do! outstream.AsyncWrite(Encoding.UTF8.GetBytes mini)
                    outstream.Close()
                    return Some(Path.GetFileName path)
                | None ->
                    return None
        }

    /// Compiles only *.coffee that have no corresponding *.js file, or where the *.js file is older than the *.coffee file.
    let updateAll () =
        async {
            let! results =
                settings.Paths
                |> Seq.collect (fun (input, output) -> 
                                    try
                                        Directory.GetFiles(input, "*.coffee", SearchOption.AllDirectories)
                                    with :? UnauthorizedAccessException -> [||])
                |> Seq.filter (fun fileName -> 
                    let jsFile = getOutPath fileName
                    not (File.Exists jsFile) || File.GetLastWriteTimeUtc jsFile < File.GetLastWriteTimeUtc fileName)
                |> Seq.map compileScript
                |> Async.Parallel
            results
            |> Array.choose id
            |> Notification.compileSuccess
        } |> Async.Start

    
    do
        watchers
        |> List.collect (fun w -> [ w.Created :> IObservable<_>; w.Changed :> IObservable<_> ])
        |> Observable.mergeAll
        |> Observable.map (fun e -> e.FullPath)
        |> Observable.buffer (TimeSpan.FromSeconds(0.5))
        |> Observable.filter (fun files -> files.Count <> 0)
        |> Observable.add (Seq.distinct >> Seq.map compileScript >> Async.Parallel >> 
                           Async.RunSynchronously >> Array.choose id >> Notification.compileSuccess)
        watchers |> List.iter (fun w -> w.EnableRaisingEvents <- true)

        updateAll()
        

    member x.CompileAll() =
        async {
            let sw = Stopwatch.StartNew()
            let! results =
                settings.Paths
                |> Seq.collect (fun (input, output) -> 
                                    try
                                        Directory.GetFiles(input, "*.coffee", SearchOption.AllDirectories)
                                    with :? UnauthorizedAccessException -> [||])
                |> Seq.map compileScript
                |> Async.Parallel
            sw.Stop()

            if results |> Array.forall (fun r -> r.IsSome) then
                results
                |> Array.choose id
                |> Array.append [| sprintf "Compiled %i files in %O." results.Length sw.Elapsed |]
                |> Notification.compileSuccess
        } |> Async.Start

    interface IDisposable with
        member x.Dispose() =
            let dispose (d: #IDisposable) = d.Dispose()
            watchers |> Seq.iter dispose